import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; import { apiSafe, type Poll } from "@/lib/api"; import { PartyBadge } from "@/components/party"; import { Badge, Card, ConfidenceBadge, KV, SectionHeader, SourceBadge } from "@/components/ui"; import { PARTY_IDS, partyVar, partyLabel } from "@/lib/parties"; import { dateFr, dateTimeFr, delta, int, modeLabel } from "@/lib/format"; export const revalidate = 120; export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { const { id } = await params; const p = await apiSafe(`/api/polls/${id}`); if (!p) return { title: "Sondage" }; return { title: `${p.pollster.name} · ${dateFr(p.fieldworkEnd)} — Poll Inspector`, description: `Sondage ${p.pollster.name} (${dateFr(p.fieldworkEnd)}) : résultats, sous-échantillons, document original et impact sur le modèle QC26.` }; } export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; const p = await apiSafe(`/api/polls/${id}`); if (!p) notFound(); const order = PARTY_IDS.slice().sort((a, b) => (p.results[b] ?? 0) - (p.results[a] ?? 0)); const subs = p.subsamples ?? {}; const dimLabel: Record = { region: "Région", language: "Langue", age: "Âge", gender: "Genre" }; return (
← Sondages{p.sourceTier === "primary" ? "Document original archivé" : "Index tiers — document en attente"}{p.excluded && Exclu du modèle}

{p.pollster.name}{p.sponsor && · {p.sponsor}}

Terrain {p.fieldworkStart ? `du ${dateFr(p.fieldworkStart)} au ` : "jusqu'au "}{dateFr(p.fieldworkEnd)} · n = {int(p.sampleSize)}{p.marginOfError ? ` · ± ${p.marginOfError.toFixed(1).replace(".", ",")} pt` : ""}{p.mode ? ` · ${modeLabel(p.mode)}` : ""}

Intentions de vote · répondants décidés{p.resultsPage ? ` · page ${p.resultsPage}` : ""}
{order.map((pid) => (
{partyLabel(pid)}
{p.results[pid] !== undefined ? p.results[pid].toFixed(1).replace(".", ",") : "—"}
))} {p.results["aut"] !== undefined &&
Autres
{p.results["aut"].toFixed(1).replace(".", ",")}
}
{p.undecided !== null &&
Indécis / discrets : {p.undecided} % de l'échantillon (exclus des pourcentages ci-dessus).
} {p.weight?.impact && (
Impact sur QC26
{PARTY_IDS.map((pid) =>
{partyLabel(pid)}
0 ? "text-ok" : (p.weight!.impact![pid] ?? 0) < 0 ? "text-live" : "text-ink-3")}>{delta(p.weight!.impact![pid], 1)}
)}
Variation de la moyenne QC26 du jour attribuable à ce sondage (recalcul sans lui). Poids dans le run : {p.weight.weight.toFixed(3)} (récence {p.weight.recency.toFixed(2)} × échantillon {p.weight.sample.toFixed(2)}).
)}
Provenance
{p.sourceUrl.replace(/^https?:\/\//, "").slice(0, 60)}… : "—"], ["Archive QC26", p.document ? {p.document.contentType?.includes("pdf") ? "PDF" : "HTML"} · {p.document.pages ?? "?"} p. · {(p.document.bytes / 1024).toFixed(0)} Ko : "—"], ["SHA-256", p.document ? {p.document.sha256} : "—"], ["Récupéré", p.document ? dateTimeFr(p.document.downloadedAt) : "—"], ["Extraction", p.extractionMethod], ["Ingéré", dateTimeFr(p.ingestedAt)]]} />
Validation
    {(p.validation?.checks ?? []).map((c, i) =>
  • ✓{c}
  • )}{(p.validation?.warnings ?? []).map((c, i) =>
  • !{c}
  • )}
{p.houseEffects && Object.keys(p.houseEffects).length > 0 &&
Effet maison {p.pollster.name} (pt) : {PARTY_IDS.map((pid) => `${partyLabel(pid)} ${delta(p.houseEffects![pid], 1)}`).join(" · ")}
}
{Object.keys(subs).length > 0 && (
{Object.entries(subs).map(([dim, segs]) => ( {PARTY_IDS.map((pid) => )}{Object.entries(segs).map(([seg, vals]) => ({PARTY_IDS.map((pid) => )}))}
{dimLabel[dim] ?? dim}n{partyLabel(pid)}p.
{seg}{vals._n ? int(vals._n as number) : "—"}{vals[pid] !== undefined && vals[pid] !== null ? Number(vals[pid]).toFixed(0) : "—"}{vals._page ?? "—"}
))}
)} {p.leadership && p.leadership.length > 0 && (
{p.leadership.map((q, i) => (
{q.question} p. {q.page}
{Object.entries(q.values).sort((a, b) => b[1] - a[1]).map(([k, v]) => (
{k}{v} %
))}
))}
)} {p.audit && p.audit.length > 0 && (
{p.audit.map((a, i) => )}
QuandQuiChampRaison
{dateTimeFr(a.ts)}{a.actor}{a.field}{a.reason}
)}
en tête dans ce sondage (répondants décidés).
); }